『Writing An Compiler In Go』
https://gyazo.com/a2946f2aae6ea30ab345c2aa1a15f269
だれかのgithub
okdmm/monkey
1章
Compilers & Virtual Mahines
コンパイラ基盤は複雑
OptimizerがASTをIR (中間表現)に変換する
その理由は、最適化のためだったり、他の言語へ変換しやすかったりするから。
IRは最適化フェーズに入る
使われてないコードを取り除いたり、事前に演算をしたり、ループに入る必要のないコードを移動させたり。
optimizerはIRを何度か舐めて、最適化を施す
1回目のパスで使われてないコードを取り除いたり、2回目のパスで関数をインライン展開したり。
プロセスVM
monkey言語においてVMを使って何が嬉しい?
移植性とかあまり関係ないやん、移植しないし
Real Machine
どのようにしてコンピュータは動いているのか
ノイマン型コンピュータ
CPUはどのようにして対象がメモリのどこにあるかを知っているのか
ノイマン型コンピュータでは、プログラムとデータは同じメモリ上にある
しかし、置かれる場所は異なる
特定の場所は特定の物のみが置かれる
メモリ
スタックマシンとレジスタマシン
switch文を使って実装すると、遅くなる
特に分岐が数百以上になったとき
fetch,decodeの部分がなくなるようにディスパッチのオーバーヘッドを大幅に減らす
他の解決策
junp tables
https://github.com/Shopify/go-lua/blob/88a6f168eee0ba102d7d20c5281056a5dd3d7550/vm.go#L306
computed GOTO statements
https://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables
indirect and direct threaded code
https://www.complang.tuwien.ac.at/forth/threaded-code.html
なぜ仮想マシンが必要か
機械語を吐くコンパイラを書けば、ネイティブの速度が出る、が、いくつかのアーキテクチャに向けたものを書かないといけなくなる
普遍的なコンピュータはユニバーサルすぎる、ドメイン固有ではまったくない
x86-64の命令とかも同じようなことをするために複数の命令があって、複雑でむずかしい
swicth文が4000分岐とかになる
この処理系用の実装もむずいし、この処理系用の言語の実装もむずくなる
バイトコード
コンパイラとVM、どっちを先に構築するか
この本では同時に両方作っていく
2章 Hello Bytecode!
こんな感じの構成になる
Lexer→Parser→Compiler→VM
「コンパイルタイム」lexer..compilerまで、
「ランタイム」はVMを指す
Strings→Tokens→AST→Bytecode→Object
constant
constant expressionの略。定数式
定数であり、コンパイル時に決定できる値のこと
コンスタントプール
整数値はその場で評価する
なんでこんなことをするか
数値の場合は普通に1と2をpushし、その後add命令を実行すれば良いが、文字列をスタックにpushしたいとなると、文字列をエンコードしてバイトコードにしてpushすることになり、できなくはないけど煩雑になっていく
そしてそれを定数としてメモリに格納する
そしてこの値にインデックスを振る
それってコンパイル時にevalしてるってこと?
VMにはコンスタントプールを渡して、インデックスを参照して結果を返す
この辺ちょっと何言ってるかわからない38/352付近
定義するデータ構造
Instructions型
seq[byte]
byteの並びで構成される
一つ一つはOpcodeか、任意個のOperand
Opcode型
byte
Opcode
OpConstant
自分で定義したopcodeの一つ
つまり1byteに収まる数値
一つのoperandを持つ
このoperandは定数に振られたインデックス
このインデックスを介して定数を取得する
そして取得した値をスタックにpushする
Definition
NameとOperandWidthを持つ
デバッグやテストで使う
あるOpcodeがいくつのoperandを持つかを対応付ける
Nameが'OpConstant'など自分で定義したopcodeの名前
OperandWidthはそれが持つoperandのbyte
例えば配列の中身が[1,2]だとすると
2つのoperandをとる
各バイト長は1byte, 2byteである
ということを表す
例えばNameが'OpConstant'のものを考える
code:nim
OpConstant: Definition(Name: "OpConstant", OperandWidths: @2)
これは2byte長のoperandを一つとる、ということがわかる
2byte長、つまる16bitなのでこれが取れる最大値は2^16-1になる
つまりuint16を表している
別にuint32にするために配列の中身を4にしてもよいのだが、
やったところで、monkey言語では65536個以上も命令を作ることはないのでやらない
もしやると未使用のバイト数が多くなり無駄遣いすることになる
makeByte(op: OpCode, operands: seq[int]): seq[byte]関数
opcodeとoperandを引数にとり、その全体をバイトコード化したものの配列を返す
本の中ではmake()という名前だが、golangのmake()関数と名前かぶってややこいのでmakeByteにした
これをテストする関数では65534をエンコードした数値のテストをしている
なんで65535ではないか
65535をエンコードすると0xFF 0xFFになる
最大値と最大値だ
これを敢えて一つ減らして65534をエンコードすることで0xFF 0xFEが正解になる
これはビッグエンディアンを採用しており、順序も込みでテストしたいのでこんなことをしているわけだ。
つまり、ビッグエンディアンにもかかわらず0xFE 0xFFと返ってくるとテストをfalseにしたい。といったことをテストするために順序が関係するような値を選んでいる
goでやってるやつ ref
code:go
b := make([]byte, 4) // サイズの指定されたからの配列を用意
binary.BigEndian.PutUint16(b0:, 0x03e8) // いれる
binary.BigEndian.PutUint16(b2:, 0x07d0)
fmt.Printf("% x\n", b)
// 03 e8 07 d0
code:go
a := []byte{255,254}
fmt.Println(binary.BigEndian.Uint16(a)) // 65534
code:nim
# イメージ
putBigEndian16(65534) # @255, 255
readUint16(@255, 255) # 65534
Compilerオブジェクト
instructions
bytecodeを保持する
constants
コンスタントプールをslice?
Bytecodeオブジェクト
instructions
コンパイラが生成したもの
constants
コンパイラが評価したもの
Bytecode disassemble
Compiling Expressions
1;2;3;というコードが実行された時、本来スタック上には3だけがあるはずだが、全部乗っている
つまり式文が沢山あれば、無駄にスタックに積まれていくことになる
これに対処するためにpopopcodeの追加と、式文の後に毎回popを入れるようにする
式の実行後に毎回popを実行する
Kindleの位置No.1844
https://cipepser.hatenablog.com/entry/go-compiler
http://junchang1031.hatenablog.com/entry/2018/12/23/081222